/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of SimpleEL.
 * 
 * SimpleEL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * SimpleEL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with SimpleEL. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.simpleel.internal.asm;

import static ch.msoftch.asm.Opcodes.*;
import java.beans.*;
import java.lang.reflect.*;
import ch.msoftch.asm.*;
import ch.msoftch.asm.Type;
import ch.msoftch.asm.tree.*;
import ch.msoftch.internal.*;

/**
 * Factory building {@link ISinglePropertyAccessor}s.<br>
 * Use this factory if you need the fastest possible implementations - at the expense of required space.<br>
 * Consider to use {@link ASMAccessorFactory} instead of this one, it's a bit slower but requires much less space!<br>
 * Only for internal use!
 * 
 * @author M. Hautle
 */
class ASMFastAccessorFactory {
    /** Index of the bean value on the stack. */
    private static final int BEAN_VAL = 1;

    /** Index of the setter value on the stack. */
    private static final int SETTER_VAL = 2;

    /** Internal name of {@link Object}. */
    private static final String OBJECT = Type.getInternalName(Object.class);

    /** Internal name of {@link ISinglePropertyAccessor}. */
    private static final String INTERFACE = Type.getInternalName(ISinglePropertyAccessor.class);

    /** Array containing the internal name of {@link Exception}. */
    private static final String[] THROW_EXCEPTION = { "java/lang/Exception" };

    /** Cached 'accessors'. */
    private static final ICache<PropertyDescriptor, ISinglePropertyAccessor> INSTANCES = CacheFactory.createCache(PropertyDescriptor.class,
            ISinglePropertyAccessor.class);

    /**
     * Returns an {@link ISinglePropertyAccessor} for the given property.<br>
     * If no such object exists, one will be created.<br>
     * If no class definition for such an accessor exists, one will be created.
     * 
     * @param p The property descriptor
     * @return A accessor instance for the given property
     * @throws IllegalArgumentException If null or an invalid type was passed
     */
    public static ISinglePropertyAccessor getAccessor(PropertyDescriptor p) throws IllegalArgumentException {
        if (p instanceof IndexedPropertyDescriptor)
            throw new IllegalArgumentException("Indexed properties were not supported!");
        // we don't care about concurrency - the cache is just to avoid unnecessary object trashing
        try {
            ISinglePropertyAccessor o = INSTANCES.get(p);
            if (o == null)
                INSTANCES.put(p, o = getAccessorClass(p).newInstance());
            return o;
        } catch (InstantiationException e) {
            throw new TechnicalException("Error while returning an recording instance", e);
        } catch (IllegalAccessException e) {
            throw new TechnicalException("Error while returning an recording instance", e);
        }
    }

    /**
     * Hidden constructor.
     */
    private ASMFastAccessorFactory() {
    }

    /**
     * Returns a accessor class from the given type.
     * 
     * @param p The desired property
     * @return The corresponding accessor class
     */
    @SuppressWarnings("unchecked")
    private static synchronized Class<? extends ISinglePropertyAccessor> getAccessorClass(PropertyDescriptor p) {
        final Class<?> c = getOwningType(p);
        final String name = buildName(c, p);
        final ASMClassLoader loader = ASMClassLoader.getLoader(c.getClassLoader());
        Class cl = loader.getLoadedClass(name);
        if (cl == null)
            cl = loader.defineClass(name, build(name, c, p));
        return cl;
    }

    /**
     * Builds the name for the accessor of the given property.
     * 
     * @param c The class
     * @param p The property
     * @return The name for the accessor
     */
    private static String buildName(final Class<?> c, PropertyDescriptor p) {
        final String name = c.getName();
        if (name.startsWith("java."))
            return "my" + name + p.getName();
        return name + "$" + p.getName();
    }

    /**
     * Returns the owing type of the given property.
     * 
     * @param p The property
     * @return The owning type
     */
    private static Class<?> getOwningType(PropertyDescriptor p) {
        final Method read = p.getReadMethod();
        return read != null ? read.getDeclaringClass() : p.getWriteMethod().getDeclaringClass();
    }

    /**
     * Builds the accessor class.
     * 
     * @param name The name of the new class
     * @param c The owning class
     * @param p The property
     * @return The bytecode
     */
    @SuppressWarnings("unchecked")
    private static byte[] build(String name, Class<?> c, PropertyDescriptor p) {
        final boolean interfaze = c.isInterface();
        final ClassNode cn = new ClassNode();
        cn.name = name.replace('.', '/');
        cn.access = ACC_PUBLIC | ACC_FINAL;
        cn.version = V1_5;
        cn.superName = OBJECT;
        cn.interfaces.add(INTERFACE);
        createDefaultConstructor(cn);
        final String type = Type.getInternalName(c);
        cn.methods.add(buildGetter(type, p, interfaze));
        cn.methods.add(buildSetter(type, p, interfaze));
        final ClassWriter w = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cn.accept(w);
        return w.toByteArray();
    }

    /**
     * Builds the setter method.
     * 
     * @param type The internal name of the owning type
     * @param p The property
     * @return The set method
     * @param interfaze True if this accessor is for a interface
     */
    private static MethodNode buildSetter(String type, PropertyDescriptor p, boolean interfaze) {
        final MethodNode m = new MethodNode(ACC_PUBLIC | ACC_FINAL, "set", "(Ljava/lang/Object;Ljava/lang/Object;)Z", null, THROW_EXCEPTION);
        final InsnList inst = m.instructions;
        final Method write = p.getWriteMethod();
        if (write != null)
            buildSetter0(type, inst, write, interfaze ? INVOKEINTERFACE : INVOKEVIRTUAL);
        else
            inst.add(new InsnNode(ICONST_0));
        inst.add(new InsnNode(IRETURN));
        return m;
    }

    /**
     * Appends the set instructions for the setter method.
     * 
     * @param type The internal name of the owning type
     * @param inst The instruction list of the {@link ISinglePropertyAccessor#set(Object, Object)} method
     * @param write The write method
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     */
    private static void buildSetter0(String type, final InsnList inst, final Method write, int invokeOpcode) {
        // cast to expected type
        inst.add(new VarInsnNode(ALOAD, BEAN_VAL));
        inst.add(new TypeInsnNode(CHECKCAST, type));
        if (write.getParameterTypes()[0].isPrimitive())
            addPrimitiveSetterBlock(write, type, inst, invokeOpcode);
        else
            addObjectSetterBlock(write, type, inst, invokeOpcode);
        inst.add(new InsnNode(ICONST_1));
    }

    /**
     * Builds the getter method.
     * 
     * @param type The internal name of the owning type
     * @param p The property
     * @return The get method
     * @param interfaze True if this accessor is for a interface
     */
    private static MethodNode buildGetter(String type, PropertyDescriptor p, boolean interfaze) {
        final MethodNode m = new MethodNode(ACC_PUBLIC | ACC_FINAL, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", null, THROW_EXCEPTION);
        final InsnList inst = m.instructions;
        final Method read = p.getReadMethod();
        if (read != null)
            buildGetter0(type, inst, read, interfaze ? INVOKEINTERFACE : INVOKEVIRTUAL);
        else
            // no read method - return undefined
            inst.add(new FieldInsnNode(GETSTATIC, INTERFACE, "UNDEFINED", "Ljava/lang/Object;"));
        inst.add(new InsnNode(ARETURN));
        return m;
    }

    /**
     * Appends the set instructions for the getter method.
     * 
     * @param type The internal name of the owning type
     * @param inst The instruction list of the {@link ISinglePropertyAccessor#get(Object)} method
     * @param read The read method
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     */
    private static void buildGetter0(String type, final InsnList inst, final Method read, int invokeOpcode) {
        // cast to expected type
        inst.add(new VarInsnNode(ALOAD, BEAN_VAL));
        inst.add(new TypeInsnNode(CHECKCAST, type));
        if (read.getReturnType().isPrimitive())
            addPrimitiveGetterBlock(read, type, inst, invokeOpcode);
        else
            addObjectGetterBlock(read, type, inst, invokeOpcode);
    }

    /**
     * Add a default constructor.
     * 
     * @param cn The classnode
     */
    @SuppressWarnings("unchecked")
    private static void createDefaultConstructor(ClassNode cn) {
        final MethodNode c = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null);
        final InsnList inst = c.instructions;
        inst.add(new VarInsnNode(ALOAD, 0));
        inst.add(new MethodInsnNode(INVOKESPECIAL, cn.superName, "<init>", "()V"));
        inst.add(new InsnNode(RETURN));
        cn.methods.add(c);
    }

    /**
     * Adds the setter instructions for the specified object property.
     * 
     * @param m The setter method
     * @param type The internal name of the type for which the accessor is built
     * @param inst The instruction list of the {@link ISinglePropertyAccessor#set(Object, Object)} method
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     */
    private static void addObjectSetterBlock(Method m, String type, InsnList inst, int invokeOpcode) {
        inst.add(new VarInsnNode(ALOAD, SETTER_VAL));
        inst.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(m.getParameterTypes()[0])));
        inst.add(new MethodInsnNode(invokeOpcode, type, m.getName(), Type.getMethodDescriptor(m)));
    }

    /**
     * Adds the setter instructions for the specified primitive property.
     * 
     * @param m The setter method
     * @param type The internal name of the type for which the accessor is built
     * @param inst The instruction list of the {@link ISinglePropertyAccessor#set(Object, Object)} method
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     */
    private static void addPrimitiveSetterBlock(Method m, String type, InsnList inst, int invokeOpcode) {
        final PrimitiveType wrapper = PrimitiveType.get(m.getParameterTypes()[0]);
        inst.add(new VarInsnNode(ALOAD, SETTER_VAL));
        inst.add(new TypeInsnNode(CHECKCAST, wrapper.wrapper));
        inst.add(new MethodInsnNode(INVOKEVIRTUAL, wrapper.wrapper, wrapper.unwrapMethod, wrapper.unwrapDescr));
        inst.add(new MethodInsnNode(invokeOpcode, type, m.getName(), Type.getMethodDescriptor(m)));
    }

    /**
     * Adds the getter instructions for the specified primitive property.
     * 
     * @param m The getter method
     * @param type The internal name of the type for which the accessor is built
     * @param inst The instruction list of the {@link ISinglePropertyAccessor#get(Object)} method
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     */
    private static void addPrimitiveGetterBlock(Method m, String type, InsnList inst, int invokeOpcode) {
        final PrimitiveType wrapper = PrimitiveType.get(m.getReturnType());
        inst.add(new MethodInsnNode(invokeOpcode, type, m.getName(), Type.getMethodDescriptor(m)));
        inst.add(new MethodInsnNode(INVOKESTATIC, wrapper.wrapper, PrimitiveType.WRAP_METHOD, wrapper.wrapDescr));
    }

    /**
     * Adds the getter instructions for the specified object property.
     * 
     * @param m The getter method
     * @param type The internal name of the type for which the accessor is built
     * @param inst The instruction list of the {@link ISinglePropertyAccessor#get(Object)} method
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     */
    private static void addObjectGetterBlock(Method m, String type, InsnList inst, int invokeOpcode) {
        inst.add(new MethodInsnNode(invokeOpcode, type, m.getName(), Type.getMethodDescriptor(m)));
    }

    /**
     * Reset the internal cache.<br>
     * Consider to use this method only for testing purposes.
     */
    public static void clearCache() {
        INSTANCES.clear();
    }
}
